package com.fuda.trade

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.accessibilityservice.GestureDescription.StrokeDescription
import android.content.Intent
import android.graphics.Path
import android.graphics.Rect
import android.os.Bundle
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.fuda.trade.model.TradeOrder
import io.zenoh.sample.Sample
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit

// Subscribes to zenoh orders topic and operates the third-party app automatically.
class TradeAccessibilityService : AccessibilityService(), ZenohManager.ZenohListener {

    companion object {
        val TAG: String = TradeAccessibilityService::class.java.simpleName
        const val SERVER_HEARTBEAT_TIMEOUT_MS = 1 * 60 * 1000L //in milliseconds
        const val PARTY3_APP_TIMEOUT_MS = 1 * 60 * 1000L // in milliseconds

        private val tradeOrderChannel = Channel<TradeOrder>(Channel.UNLIMITED)
        private val tradeOrderCoroutineScope = CoroutineScope(Dispatchers.IO)
        private val serverHeartbeatCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
        private val party3AppCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
        private var lastServerHeartbeatTime: Long = System.currentTimeMillis()
        private val foregroundApps: HashMap<String, Long> = HashMap<String, Long>()

    }

    override fun onServiceConnected() {
        super.onServiceConnected()
        Log.e(TAG, "Accessibility service connected.")
        MyApplication.zenohManager.subscribe(ZenohManager.ordersKeyExpr!!, this   )
        MyApplication.zenohManager.subscribe(ZenohManager.heartbeatKeyExpr!!, this   )

        tradeOrderCoroutineScope.launch {
            for (order in tradeOrderChannel){
                handleOrder(order)
            }
        }

        serverHeartbeatCoroutineScope.launch {
            checkServerHeartbeat()
        }


        party3AppCoroutineScope.launch {
            checkParty3AppActive()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.e(TAG, "Accessibility service onDestroy.")
        MyApplication.zenohManager.unsubscribe(ZenohManager.ordersKeyExpr!!, this   )
        MyApplication.zenohManager.unsubscribe(ZenohManager.heartbeatKeyExpr!!, this   )
        tradeOrderCoroutineScope.cancel()
        serverHeartbeatCoroutineScope.cancel()
        party3AppCoroutineScope.cancel()

    }

    // Subscriber received Sample from Zenoh topic
    override fun onZenohSampleReceived(sample: Sample) {
        Log.e(TAG, ">> Received sample= $sample")
        // Have to use keyExpr.toString() to compare, 不能用对象比较
        when (val keyString = sample.keyExpr.toString()){
            ZenohManager.TRADE_ORDERS_TOPIC->{
                val payload = sample.value.toString()
                val tradeOrder = TradeOrder.deserialize(payload)
                tradeOrderCoroutineScope.launch {
                    tradeOrderChannel.send(tradeOrder)
                }
            }
            ZenohManager.ZENOH_HEARTBEAT_TOPIC->{
                lastServerHeartbeatTime = System.currentTimeMillis()
            }
            else->{
                Log.e(TAG,"Unknown $keyString ")
            }
        }
    }

    override fun onAccessibilityEvent(event: AccessibilityEvent) {
        val rootNode = rootInActiveWindow ?: return
        val packageName = event.packageName.toString()

        // The "part3 app" is now in the foreground.
        handleParty3AppInForeground(packageName)

        // TradeAccessibilityService won't do anything onAccessibilityEvent
        val debug = false
        if (debug) {
            MyApplication.zenohManager.subscribe(ZenohManager.ordersKeyExpr!!, this   )

            val path =intArrayOf(0,0,4,1)
            val foundNode = findChildNodeByPath(rootNode, path)
            val pathStr = path.joinToString(prefix = "[", postfix = "]", separator = ", ")

            Log.d(TAG, "onAccessibilityEvent rootNode= $rootNode, \nfoundNode(rootNode,$pathStr)= $foundNode,  \nevent= $event")

            val listeners = MyApplication.zenohManager.listeners

            val payload =
                String.format("@onAccessibilityEvent, listeners= $listeners, \nevent= ${event}")
            MyApplication.zenohManager.publish(ZenohManager.responseKeyExpr!!, payload)
        }
    }



    override fun onUnbind(intent: Intent?): Boolean {
        return super.onUnbind(intent)
    }

    override fun onInterrupt() {
    }

    private suspend fun checkServerHeartbeat() {
        // Check for timeout every second
        while (true) {
            // Check if the last heartbeat was received more than HEARTBEAT_TIMEOUT_MS ago
            val currentTime = System.currentTimeMillis()
            if (currentTime - lastServerHeartbeatTime > SERVER_HEARTBEAT_TIMEOUT_MS) {
                Log.e(TAG, "Heartbeat timeout: No heartbeat received within $SERVER_HEARTBEAT_TIMEOUT_MS ms.")
                lastServerHeartbeatTime = currentTime // Reset the timestamp to avoid continuous logging

                // Start HeartbeatDialog
                startMainActivity("SERVER_HEARTBEAT_TIMEOUT", "")
            }

            // Delay for 3 second before checking again
            delay(3000L)
        }
    }

    private suspend fun checkParty3AppActive() {
        // Check for timeout every second
        while (true) {
            val currentTime = System.currentTimeMillis()

            // Check if the party3 app is not in foreground for more than HEARTBEAT_TIMEOUT_MS
            if (!MyApplication.party3AppManager.isAppActive(this)) {
                Log.e(TAG, "Cczq(川财证券) App Timeout: Not in foreground within $PARTY3_APP_TIMEOUT_MS ms.")
                lastServerHeartbeatTime = currentTime // Reset the timestamp to avoid continuous logging

                MyApplication.party3AppManager.refreshActiveForeground(this)
            }

            // Delay for 1 second before checking again
            delay(PARTY3_APP_TIMEOUT_MS/2)
        }
    }


    private fun handleOrder(tradeOrder: TradeOrder) {
        Log.i(TAG, "   @handleOrder= $tradeOrder")
        val startTime = System.currentTimeMillis()

        val payload = String.format("($tradeOrder) processed@handleOrder-${TAG}")
        MyApplication.zenohManager.publish(ZenohManager.responseKeyExpr!!, payload)

        MyApplication.party3AppManager.handleOrder(this,tradeOrder)

        logElapsedTime(TAG, startTime)
    }

    private fun handleParty3AppInForeground(packageName: String) {
        // Check if the app is the party3 app we requested
        if (packageName == "com.tdx.androidCCZQ"
            //|| packageName =="com.guosen.android"
            ) {
            Log.d(TAG, "(handleParty3AppInForeground) CCZQ App is in Foreground")
            // Store the current timestamp for the package name
            foregroundApps[packageName] = System.currentTimeMillis()
        } else {
            // Remove the package if it was previously active but is no longer in the foreground
            foregroundApps.remove(packageName)
        }
    }

    /**
     * check whether the party3 app is in foreground within the {timeoutMs} milliseconds:
      */
    fun isParty3AppActive(packageName: String?, timeoutMs: Long): Boolean {
        val timestamp = foregroundApps[packageName]
        if (timestamp != null) {
            val currentTime = System.currentTimeMillis()
            val elapsed = currentTime - timestamp
            return elapsed <= timeoutMs
        }
        return false
    }

    // 回退
    fun globalGoBack():Boolean {
        val result = performGlobalAction(GLOBAL_ACTION_BACK)
        TimeUnit.MILLISECONDS.sleep(100)
        return result
    }

    // 返回手机首页
    fun globalGoHome():Boolean {
        val result = performGlobalAction(GLOBAL_ACTION_HOME)
        TimeUnit.MILLISECONDS.sleep(100)
        return result
    }


    // party3 app ui operators
    fun waitForTextAndTouch(rect: Rect, text:String, timeoutMillis:Int) : Boolean{
        var result = false
        Log.e(
            TAG,
            String.format(
                "(simulateTouch) Simulate Touch rect= [%d, %d, %d, %d] text= $text ",
                rect.left, rect.top, rect.right, rect.bottom
            ),
        )

        // Calculate the center of the bounds
        val centerX = rect.centerX().toFloat()
        val centerY = rect.centerY().toFloat()

        val tap: Path = Path()
        tap.moveTo(centerX, centerY)

        val gesture = GestureDescription.Builder().addStroke(StrokeDescription(tap, 0, 500)).build()
        result =  dispatchGesture(gesture, null, null)

        TimeUnit.MILLISECONDS.sleep(500)

        return result
    }

    // party3 app ui operators
    fun waitForViewAndClick(viewId: String, timeoutMillis:Int ): Boolean  {
        var result = false
        val startTime = System.currentTimeMillis()
        while (System.currentTimeMillis() - startTime < timeoutMillis) { // Wait up to 10 seconds
            val rootNode = rootInActiveWindow
            if (rootNode != null) {
                val node = rootNode.findAccessibilityNodeInfosByViewId(viewId).firstOrNull()
                if (node != null && node.isClickable) {
                    Log.e(TAG, "(waitForViewAndClick) Found the view with ID $viewId, perform ACTION_CLICK.")
                    node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                    result =  true
                    break;
                }
            }else{
                Log.e(TAG, "(waitForViewAndClick) Could not find rootNode")
            }
            Thread.sleep(500) // Sleep for 500ms before checking again
        }
        if (!result) {
            Log.e(TAG, "(waitForViewAndClick) Could not find the view with ID $viewId after waiting.")
        }
        return result
    }

    // party3 app ui operators
    fun waitForPathAndClick(path: IntArray, timeoutMillis:Int ): Boolean  {

        var result = false
        val pathStr = path.joinToString(prefix = "[", postfix = "]", separator = ", ")
        val startTime = System.currentTimeMillis()
        while (System.currentTimeMillis() - startTime < timeoutMillis) { // Wait up to 10 seconds
            val rootNode = rootInActiveWindow
            if (rootNode != null) {
                val node = findChildNodeByPath(rootNode,path)

                if (node != null ) {
                    if (node.isClickable ) {
                        Log.d( TAG, "(waitForPathAndClick) Found the view with path= $pathStr, perform ACTION_CLICK. \nnode= $node" )
                        node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                        result= true
                        break
                    }else if(node.isLongClickable){
                        Log.d( TAG, "(waitForPathAndClick) Found the view with path= $pathStr, perform ACTION_LONG_CLICK. \nnode= $node" )
                        node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
                        result =  true
                        break
                    }else {
                        Log.e( TAG, "(waitForPathAndClick) Found the view with path= $pathStr, not Clickable nor LongClickable. \nnode= $node" )
                    }
                }
            }else{
                Log.e(TAG, "(waitForPathAndClick) Could not find rootNode")
            }
            TimeUnit.MILLISECONDS.sleep(500)
        }
        if (!result) {
            Log.e(TAG, "(waitForPathAndClick) Could not find the view with path= $pathStr after waiting.")
        }

        TimeUnit.MILLISECONDS.sleep(500)
        return result
    }

    // party3 app ui operators
    fun waitForPathAndEdit(path: IntArray,value: String,  timeoutMillis:Int ): Boolean  {
        var result = false
        val pathStr = path.joinToString(prefix = "[", postfix = "]", separator = ", ")

        val startTime = System.currentTimeMillis()
        while (System.currentTimeMillis() - startTime < timeoutMillis) { // Wait up to 10 seconds
            val rootNode = rootInActiveWindow
            if (rootNode != null) {
                val node = findChildNodeByPath(rootNode,path)

                if (node != null ) {
                    if (node.isClickable){
                        node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
                    }else if (node.isLongClickable){
                        node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
                    }
                    if (node.isEditable ) {
                        Log.d( TAG, "(waitForPathAndEdit) Found the view with path= $pathStr, perform ACTION_SET_TEXT. \nnode= $node" )

                        // Ensure the EditText is focused before setting the text
                        if (!node.isFocused) {
                            node.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
                            TimeUnit.MILLISECONDS.sleep(500) // Wait for the EditText to gain focus
                        }

                        // Check if the EditText is enabled
                        if (node.isEnabled) {
                            val bundle = Bundle().apply {
                                putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value)
                            }

                            // Perform the ACTION_SET_TEXT action
                            val actionResult = node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, bundle)

                            if (actionResult) {
                                Log.e( TAG, "(waitForPathAndEdit) ACTION_SET_TEXT performed successfully." )
                                result = true
                                break
                            } else {
                                Log.e( TAG, "(waitForPathAndEdit) ACTION_SET_TEXT failed." )
                            }
                        } else {
                            Log.e( TAG, "(waitForPathAndEdit) EditText is NOT Enabled." )
                        }
                    }else {
                        Log.e( TAG, "(waitForPathAndEdit) Found the view with path= $pathStr, not Editable. \nnode= $node" )
                    }
                }
            }else{
                Log.e(TAG, "(waitForPathAndEdit) Could not find rootNode")
            }
        }
        if (!result) {
            Log.e(TAG, "(waitForPathAndEdit) Could not find the view with path= $pathStr after waiting.")
        }
        TimeUnit.MILLISECONDS.sleep(500)
        return result
    }

    // party3 app ui operators
    fun waitForViewAndInput(viewId: String, value: String, timeoutSeconds:Int ): Boolean {
        val startTime = System.currentTimeMillis()
        while (System.currentTimeMillis() - startTime < timeoutSeconds) { // Wait up to 10 seconds
            val rootNode = rootInActiveWindow
            if (rootNode != null) {
                val editText = rootNode.findAccessibilityNodeInfosByViewId(viewId).firstOrNull()
                if (editText != null) {
                    Log.e(TAG, "Found the EditText with ID $viewId, input text: $value.")
                    editText.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, Bundle().apply {
                        putCharSequence( AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value)
                    })
                    return true
                }
            }else{
                Log.e(TAG, "Could not find rootNode")
            }
            Thread.sleep(500) // Sleep for 500ms before checking again
        }
        Log.e(TAG, "Could not find the EditText with ID $viewId after waiting.")
        return false
    }

    /**
     * Waits for a specific text to appear and then disappear from the screen.
     *
     * @param text The text to wait for appearance and disappearance.
     * @param timeoutMillis The maximum time to wait in milliseconds.
     * @return True if the text disappears after appearing within the timeout, false otherwise.
     */
    fun waitForTextAppearAndDisappear(text: String, timeoutMillis: Long = 10000L): Boolean {
        var result = false
        val startTime = System.currentTimeMillis()
        val endTime = startTime + timeoutMillis

        // First, check if the text is present
        var textIsPresent = findTextInCurrentView(text)

        // Wait for the text to appear
        while (System.currentTimeMillis() < endTime && !textIsPresent) {
            // Wait a bit before checking again
            TimeUnit.MILLISECONDS.sleep(500)

            // Check if the text is now present
            textIsPresent = findTextInCurrentView(text)
        }

        // If the text never appeared within the timeout, return false
        if (!textIsPresent) {
            Log.e(TAG, "(waitForTextAppearAndDisappear) Text '$text' did not appear within the timeout.")
            return false
        }

        // Now, wait for the text to disappear
        while (System.currentTimeMillis() < endTime && textIsPresent) {
            // Wait a bit before checking again
            TimeUnit.MILLISECONDS.sleep(500)

            // Check if the text is still present
            textIsPresent = findTextInCurrentView(text)
        }

        if (!textIsPresent) {
            result = true
        } else {
            Log.e(TAG, "(waitForTextAppearAndDisappear) Text '$text' did not disappear within the timeout.")
        }

        return result
    }

    /**
     * Checks if the specified text is present in the current view.
     *
     * @param text The text to search for.
     * @return True if the text is present, false otherwise.
     */
    private fun findTextInCurrentView(text: String): Boolean {
        val rootNode = rootInActiveWindow ?: return false

        // Check if the text is present in the active window
        return rootNode.findAccessibilityNodeInfosByText(text).isNotEmpty()
    }


    // Helper method to find the view by path
    // Define the path as an array of indices
    // val path = intArrayOf( 0, 1 )
    // For example, the 1st child of the root, followed by the 2nd child of that node
    private fun findChildNodeByPath( root: AccessibilityNodeInfo?, path: IntArray? ): AccessibilityNodeInfo? {
        if (root == null || path == null || path.isEmpty()) {
            return null
        }

        var currentNode = root
        for (index in path) {
            if (currentNode == null || index < 0 || index >= currentNode.childCount) {
                return null
            }

            currentNode = currentNode.getChild(index)
        }

        return currentNode
    }


    fun startMainActivity(startReason: String, payload: String) {
        globalGoHome()
        try {
            // Get the application context from the service

            // Create an Intent to start MainActivity
            val intent = Intent(this, MainActivity::class.java)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
            intent.putExtra("START_REASON", startReason)
            intent.putExtra("PAYLOAD",payload)
            // Start the activity
            applicationContext.startActivity(intent)
        } catch (e: Exception) {
            Log.e(TAG, "Failed to start MainActivity", e)
        }
    }



}